﻿/**
 * @author Zack "Genesis2001" Loveless
 * @package Atlantis.Net.Irc
 * @version $Id $
 * @copyright (c) 2009 CnCFPS Development Group, Zack Loveless
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 * 
 */

/*
 * NOTES:
 * 
 * 1. DateTime Formatting: http://www.dotnetspider.com/resources/266-Formatting-Date-Time-using-e-DateTime-object.aspx
 * 
 */

namespace Atlantis.Net.Irc
{
    using Atlantis;
    using Atlantis.Linq;

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading;

    public class IrcClient : IAPIClient
    {

        #region Constructor(s)

        public IrcClient()
        {
            Channels = new List<Channel>(100);
            Usermodes = new List<char>(50);
        }

        #endregion

        #region Fields

        private string m_LogFile = String.Empty;
        private NetworkStream m_Stream;
        private Thread m_Thread;

        #region IRCd Variables

        private string m_IrcdChanModes = String.Empty;
        private string m_IrcdPrefixes = String.Empty;
        private string m_IrcdModes = String.Empty;

        #endregion

        #region Regular Expressions

        private Regex ircdPrefix = new Regex(@"PREFIX=\((\S+)\)(\S+)"); // PREFIX=(qaohv)~&@%+
        private Regex userHost = new Regex(@":([^!]+)!([^@]+@\S+)"); // :Global!services@phantomlink.net
        private Regex ircdChanModes = new Regex("CHANMODES=(\\S+)"); // CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMTGy
        // private Regex ircdCaseMap = new Regex("CASEMAPPING=(\\S+)"); // CASEMAPPING=ascii
        private Regex ircdNamesUser = null;
        private Regex ircdModeSet = new Regex(@"^:?(\S+) MODE (\S+) :?(.*)");
        private Regex ircdPrvimsg = new Regex(@"^:?(\S+) PRIVMSG (\S+) :(.*)");
        private Regex ircdNotice = new Regex(@"^:?(\S+) NOTICE (\S+) :(.*)");
        private Regex ircdUserQuit = new Regex(@"^:?(\S+) QUIT :(.*)");
        private Regex ircdNickChange = new Regex(@"^:?(\S+) NICK :(\S+)"); // :Genesis2001!zack@cncfps.com NICK :Test

        #endregion

        #endregion

        #region Properties

        private List<Channel> m_Channels = null;
        /// <summary>
        ///     Gets a list of channels the client is currently on
        /// </summary>
        public List<Channel> Channels
        {
            get { return m_Channels; }
            private set { m_Channels = value; }
        }

        private TcpClient m_Client;
        /// <summary>
        ///     Gets the underlying TcpClient supporting the IrcClient
        /// </summary>
        public TcpClient Client
        {
            get { return m_Client; }
            private set { m_Client = value; }
        }

        private Encoding m_Encoding = Encoding.ASCII;
        /// <summary>
        ///     Gets or sets the encoding of the text that is read and written to IRC
        /// </summary>
        public Encoding Encoding
        {
            get { return m_Encoding; }
            set { m_Encoding = value; }
        }

        private string m_Ident = String.Empty;
        /// <summary>
        ///     Gets or sets the Identity of the client
        /// </summary>
        public string Ident
        {
            get { return m_Ident; }
            set { m_Ident = value; }
        }

        /// <summary>
        ///     Gets a value indicating whether the IrcClient has been initialized pre-connection state.
        /// </summary>
        public bool IsInitialized
        {
            get
            {
                bool ret = true;

                if (String.IsNullOrEmpty(Ident)) ret = false;
                if (String.IsNullOrEmpty(Host)) ret = false;
                if (String.IsNullOrEmpty(Nick)) ret = false;
                if (String.IsNullOrEmpty(Realname)) ret = false;

                return ret;
            }
        }

        private string m_Host = String.Empty;
        /// <summary>
        ///     Gets or sets the host of the IRC server the client is connecting to
        /// </summary>
        public string Host
        {
            get { return m_Host; }
            set { m_Host = value; }
        }

        /// <summary>
        ///     Gets or sets a value indicating whether the IRC Thread is a background thread or if it blocks.
        /// </summary>
        /// <exception cref="System.Threading.ThreadStateException" />
        public bool IsBackground
        {
            get { return m_Thread.IsBackground; }
            set { m_Thread.IsBackground = value; }
        }

        private string m_Nick = String.Empty;
        /// <summary>
        ///     Gets or sets the Nickname of the client.
        /// </summary>
        public string Nick
        {
            get { return m_Nick; }
            set { m_Nick = value; }
        }

        private string m_Password = String.Empty;
        /// <summary>
        ///     Gets or sets the connection password to be sent upon connecting to the IRC server
        /// </summary>
        public string Password
        {
            get { return m_Password; }
            set { m_Password = value; }
        }

        private int m_Port = 6667;
        /// <summary>
        ///     Gets or sets the port to use when connecting to the IRC server
        /// </summary>
        public int Port
        {
            get { return m_Port; }
            set { m_Port = value; }
        }

        private string m_Realname = String.Empty;
        /// <summary>
        ///     Gets or sets the client's Realname
        /// </summary>
        public String Realname
        {
            get { return m_Realname; }
            set { m_Realname = value; }
        }

        private bool m_StrictNames = false;
        /// <summary>
        ///     Gets or sets a value determining whether to always request NAMES upon any action that involves users leaving, quitting, being kicked, etc.
        ///     <para>Default: false</para>
        /// </summary>
        public bool StrictNames
        {
            get { return m_StrictNames; }
            set { m_StrictNames = value; }
        }

        private List<char> m_Usermodes = null;
        public List<char> Usermodes
        {
            get { return m_Usermodes; }
            private set { m_Usermodes = value; }
        }

        private bool m_WriteLog = false;
        /// <summary>
        ///     Gets or sets a value indicating whether IRC output received from should be written to "ircdebug_M-D-YYYY.txt" in the program's output directory
        /// </summary>
        public bool WriteLog
        {
            get { return m_WriteLog; }
            set { m_WriteLog = value; }
        }

        #endregion

        #region Events

        public event EventHandler<ChannelMessageReceiveEventArgs> OnChannelMessageReceive;
        public event EventHandler<EventArgs> OnConnect;
        public event EventHandler<DebugEventArgs> DebugEvent;
        public event EventHandler<JoinPartEventArgs> OnJoin;
        public event EventHandler<NickChangeEventArgs> OnNickChange;
        public event EventHandler<JoinPartEventArgs> OnPart;
        public event EventHandler<PrivateMessageReceivedEventArgs> OnPrivateMessageReceive;
        public event EventHandler<QuitEventArgs> OnUserQuit;
        public event EventHandler<RawEventArgs> OnRaw;
        public event EventHandler<EventArgs> OnUsermodeChange;

        #endregion

        #region Methods

        public static IrcClient Connect(string hostOrIpAddress, int port, string clientName, string clientIdent, string clientRealname)
        {
            IrcClient ret = new IrcClient();

            ret.Host = hostOrIpAddress;
            ret.Port = port;
            ret.Nick = clientName;
            ret.Ident = clientIdent;
            ret.Realname = clientRealname;

            ret.Start();

            return ret;
        }

        public bool Disconnect()
        {
            if (m_Client != null && m_Client.Connected)
            {
                Send("QUIT");
                m_Thread.Abort();
                m_Client.Close();
                return true;
            }
            return false;
        }

        public bool Disconnect(string format, params object[] args)
        {
            if (m_Client != null && m_Client.Connected)
            {
                Send("QUIT :{0}", String.Format(format, args));
                m_Thread.Abort();
                m_Client.Close();
                return true;
            }
            return false;
        }

        protected virtual void OnDataReceive(string data)
        {
            if (WriteLog)
            {
                WriteToLog(data);
                DebugEvent.Raise(this, new DebugEventArgs(data));
            }

            string[] toks = data.Split(' ');
            Match m;

            if (toks[0].Equals("PING", StringComparison.CurrentCultureIgnoreCase))
            {
                Send("PONG {0}", toks[1]);
            }

            int raw = -1;
            if (Int32.TryParse(toks[1], out raw))
            {
                OnRaw.Raise(this, new RawEventArgs(raw, data));

                if (raw == 001)
                {
                    OnConnect.Raise(this, new EventArgs());
                }
                else if (raw == 005)
                {
                    if ((m = ircdPrefix.Match(data)).Success)
                    {
                        m_IrcdModes = m.Groups[1].Value;
                        m_IrcdPrefixes = m.Groups[2].Value;
                        ircdNamesUser = new Regex(String.Format(@"([{0}]?)(\S+)", m_IrcdPrefixes));
                    }
                    if ((m = ircdChanModes.Match(data)).Success)
                    {
                        m_IrcdChanModes = m.Groups[1].Value;
                    }
                }
                else if (raw == 353)
                {
                    Channel c = GetChannel(toks[4]);

                    string sub = data.Substring(data.IndexOf(":", 1)).Remove(0, 1);
                    string[] names = sub.Split(' ');

                    foreach (var s in names)
                    {
                        if (String.IsNullOrEmpty(s))
                        {
                            continue;
                        }

                        if ((m = ircdNamesUser.Match(s)).Success)
                        {
                            var nick = m.Groups[2].Value;
                            var access = (String.IsNullOrEmpty(m.Groups[1].Value) ? '\0' : m.Groups[1].Value[0]);
                            if (c.Users.ContainsKey(nick))
                            {
                                c.Users[nick] = access;
                            }
                            else
                            {
                                c.Users.Add(nick, access);
                            }
                            //Send("PRIVMSG {0} :Nick: {1} - Access: {2}", c.Name, m.Groups[2].Value, m.Groups[1].Value);
                        }
                    }
                }
            }
            else if (toks[1].Equals("JOIN", StringComparison.CurrentCultureIgnoreCase))
            {
                // :Atlantis!staff@5B496B2C.715924CF.74B597C1.IP JOIN :#cncfps
                if ((m = userHost.Match(toks[0])).Success)
                {
                    string chan = toks[2].Remove(0, 1);
                    string nick = m.Groups[1].Value;

                    Channel c = GetChannel(chan);

                    if (!c.Users.ContainsKey(nick))
                    {
                        c.Users.Add(nick, '\0');
                    }

                    OnJoin.Raise(this, new JoinPartEventArgs(nick, c.Name, m.Groups[0].Value));

                    if (StrictNames)
                    {
                        Send("NAMES {0}", c.Name);
                    }
                }
            }
            else if (toks[1].Equals("PART", StringComparison.CurrentCultureIgnoreCase))
            {
                if ((m = userHost.Match(toks[0])).Success)
                {
                    var channel = toks[2];
                    var nick = m.Groups[1].Value;

                    OnPart.Raise(this, new JoinPartEventArgs(nick, channel, m.Groups[0].Value));

                    Channel c = GetChannel(channel);

                    if (c.Users.ContainsKey(nick))
                    {
                        c.Users.Remove(nick);
                    }

                    if (StrictNames)
                    {
                        Send("NAMES {0}", channel);
                    }
                }
            }
            else if ((m = ircdModeSet.Match(data)).Success)
            {
                //^:?(\S+) MODE (\S+) (.*)

                // TODO: Parse.
                //:CnCFPS!services@cncfps.com MODE #cncfps +qo Genesis2001 Genesis2001
                //CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMTGy
                string modes = m.Groups[3].Value.Split(' ')[0];
                string parameters = "";
                if (m.Groups[3].Value.Split(' ').Length > 1)
                {
                    parameters = m.Groups[3].Value.Substring(m.Groups[3].Value.IndexOf(' ') + 1);
                }

                string[] chanModes = m_IrcdChanModes.Split(',');
                string modeList = chanModes[0];     // these modes are list modes such as bans and invites +b +i
                string modeSetUnset = chanModes[1];     // these modes need parameters to set or unset them
                string modeSet = chanModes[2];      // these modes need parameters to set only
                string modeNone = chanModes[3];     // these modes do not require parameters
                //                 m_IrcdModes      // user access modes
                string[] paramToks = parameters.Split(' ');
                //Console.WriteLine("parameters: {0}", parameters);

                bool set = true;
                int param = -1;

                //bool userModeChange = false;
                char[] modeStr = modes.ToCharArray();
                foreach (var c in modeStr)
                {
                    if (c == '+')
                    {
                        set = true;
                        continue;
                    }
                    else if (c == '-')
                    {
                        set = false;
                        continue;
                    }

                    if (m.Groups[2].Value.Equals(Nick, StringComparison.CurrentCultureIgnoreCase))
                    {
                        if (set)
                        {
                            Usermodes.Add(c);
                        }
                        else
                        {
                            Usermodes.Remove(c);
                        }
                        OnUsermodeChange.Raise(this, new EventArgs());
                    }
                    else
                    {
                        Channel chan = GetChannel(toks[2]);
                        if (m_IrcdModes.Contains(c))
                        {
                            param++;
                            //userModeChange = true;
                            string u = paramToks[param];
                            if (chan.Users.ContainsKey(u))
                            {
                                if (set)
                                {
                                    chan.Users[u] = m_IrcdPrefixes[m_IrcdModes.IndexOf(c)];
#if DEBUG
                                    Debug.WriteLine(String.Format("\t\t{0} has been given {1} on {2}", u, chan.Users[u], chan.Name));
#endif
                                    if (StrictNames)
                                    {
                                        Send("NAMES {0}", chan.Name);
                                    }
                                }
                                else
                                {
                                    char mode = (m_IrcdPrefixes.Contains(c) ? m_IrcdPrefixes[m_IrcdModes.IndexOf(c)] : '\0');
                                    // mode will be '\0' if and only if there is an unrecognized chan-usermode being set.
                                    if (mode != '\0' && chan.Users[u].Equals(mode))
                                    {
                                        chan.Users[u] = '\0';
                                        if (StrictNames)
                                        {
                                            Send("NAMES {0}", chan.Name);
                                        }
                                    }
#if DEBUG
                                    Debug.WriteLine(String.Format("\t\t{0} has been revoked {1} on {2}", u, m_IrcdPrefixes[m_IrcdModes.IndexOf(c)], chan.Name));
#endif
                                }
                            }
                        }
                        else if (modeSet.Contains(c))
                        {
                            if (set)
                            {
                                param++;
#if DEBUG
                                string tmp = paramToks[param];
                                Debug.WriteLine(String.Format("(modeSet) Mode: {0}, Associated Parameter: {1}", c, tmp));
#endif
                            }
                        }
                        else if (modeSetUnset.Contains(c))
                        {
                            param++;
#if DEBUG
                            string tmp = paramToks[param];
                            Debug.WriteLine(String.Format("(modeSetUnset) Mode: {0}{1}, Associated Parameter: {2}", (set ? '+' : '-'), c, tmp));
#endif
                        }
                        else if (modeList.Contains(c))
                        {
                            param++;
#if DEBUG
                            string tmp = paramToks[param];
                            Debug.WriteLine(String.Format("(modeList) Mode: {0}{1}, Associated Parameter: {2}", (set ? '+' : '-'), c, tmp));
#endif
                        }
                        else
                        {
#if DEBUG
                            Debug.WriteLine(String.Format("Unhandled mode: {0}{1}", (set ? '+' : '-'), c));
#endif
                        }
                    }
                }
            }
            else if ((m = ircdPrvimsg.Match(data)).Success)
            {
                //^:?(\S+) PRIVMSG (\S+) :(.*)

                string u = "";
                if (userHost.IsMatch(toks[0]))
                {
                    u = userHost.Match(toks[0]).Groups[1].Value;
                }

                string target = m.Groups[2].Value;
                string message = m.Groups[3].Value;

                if (target[0].Equals('#'))
                {
                    OnChannelMessageReceive.Raise(this, new ChannelMessageReceiveEventArgs(GetChannel(target), message, u, m.Groups[1].Value, false));
                }
                else
                {
                    OnPrivateMessageReceive.Raise(this, new PrivateMessageReceivedEventArgs(target, message, u, m.Groups[1].Value, false));
                }
                //Console.WriteLine("Sender: {0}, Target: {1}, Message: {2}", (String.IsNullOrEmpty(u) ? "NULL" : u), target, message);

                //:NickServ!services@jelly-server.com NOTICE Atlantis :please choose a different nick.
            }
            else if ((m = ircdNotice.Match(data)).Success)
            {
                string u = "";
                if (userHost.IsMatch(toks[0]))
                {
                    u = userHost.Match(toks[0]).Groups[1].Value;
                }

                string target = m.Groups[2].Value;
                string message = m.Groups[3].Value;

                if (target[0].Equals('#'))
                {
                    OnChannelMessageReceive.Raise(this, new ChannelMessageReceiveEventArgs(GetChannel(target), message, u, m.Groups[1].Value, true));
                }
                else
                {
                    OnPrivateMessageReceive.Raise(this, new PrivateMessageReceivedEventArgs(target, message, u, m.Groups[1].Value, true));
                }
            }
            else if ((m = ircdUserQuit.Match(data)).Success)
            {
                string u = "";
                if (userHost.IsMatch(toks[0]))
                {
                    u = userHost.Match(toks[0]).Groups[1].Value;
                }
                //:Lone0001!lone0001@lone0001.genesisblog.net QUIT :[irc.cncfps.com] Local kill by Genesis2001 (Thanks for volunteering for my test.)
                if (!String.IsNullOrEmpty(u))
                {
                    foreach (var c in Channels)
                    {
                        if (c.Users.ContainsKey(u))
                        {
                            c.Users.Remove(u);
                        }
                    }

                    OnUserQuit.Raise(this, new QuitEventArgs(u, m.Groups[2].Value));
                }
            }
            else if ((m = ircdNickChange.Match(data)).Success)
            {
                string o = "";
                if (userHost.IsMatch(toks[0]))
                {
                    o = userHost.Match(toks[0]).Groups[1].Value;
                }

                string n = m.Groups[2].Value;
                if (!String.IsNullOrEmpty(o) && !String.IsNullOrEmpty(n))
                {
                    foreach (var c in Channels)
                    {
                        if (c.Users.ContainsKey(o))
                        {
                            char t = c.Users[o];
                            c.Users.Remove(o);
                            c.Users.Add(n, t);
                        }
                    }
                    OnNickChange.Raise(this, new NickChangeEventArgs(o, n));
                }
            }
        }

        /// <summary>
        ///     Returns a channel based on the specified name.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public Channel GetChannel(string name)
        {
            Channel ret = Channels.OfType<Channel>().SingleOrDefault(c => c.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase));

            if (ret == null)
            {
                ret = new Channel(name, this);
                Channels.Add(ret);
            }

            return ret;
        }

        /// <summary>
        ///     Sends the specified System.String to the IRC connection
        /// </summary>
        /// <param name="data"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public bool Send(string data, params object[] args)
        {
            if (m_Client != null && m_Client.Connected)
            {
                string tmp = String.Format(data, args);
                byte[] bData = Encoding.GetBytes(String.Format("{0}\n", tmp));

                m_Stream.Write(bData, 0, bData.Length);
                m_Stream.Flush();

                return true;
            }

            return false;
        }


        public void Start()
        {
            if (!IsInitialized)
            {
                throw new NotImplementedException("All required properties are not set. Please see the documentation.");
            }

            Client = new TcpClient();
            m_Thread = new Thread(new ThreadStart(ThreadCallback));
            m_Thread.Start();
        }

        public void Stop()
        {
            // 
        }

        private void ThreadCallback()
        {
            Client.Connect(Host, Port);
            m_Stream = Client.GetStream();

#if DEBUG
            Debug.WriteLine(String.Format("Establishing connection on port {0} to {1}", Port, Host));
#endif

            if (!String.IsNullOrEmpty(Password))
            {
                Send("PASS :{0}", Password);
            }

            Send("NICK {0}", Nick);
            Send("USER {0} 0 * : {1}", Ident, Realname);

            string data = null;

            do
            {
                if (m_Stream.DataAvailable)
                {
                    using (StreamReader stream = new StreamReader(m_Stream, Encoding))
                    {
                        while (!stream.EndOfStream)
                        {
                            data = stream.ReadLine();

                            if (!String.IsNullOrEmpty(data))
                            {
                                OnDataReceive(data);
                            }

#if DEBUG
                            Debug.WriteLine(data);
#endif
                        }
                    }
                }
            } while (m_Client.Connected);
        }

        private void WriteToLog(string data)
        {
            string logfile = String.Format("ircdebug_{0}.txt", DateTime.Now.ToString("M-d-yyyy"));
            if (!File.Exists(logfile))
            {
                File.Create(logfile).Close();
            }

            string tmp = String.Format("[{0}] {1}\n", DateTime.Now.ToString("hh:mm:ss"), data);
            File.AppendAllText(logfile, tmp);
        }

        #endregion

    }
}
